<?php
// Copyright 2011 Zeptonic Ek. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
//    conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
//    of conditions and the following disclaimer in the documentation and/or other materials
//    provided with the distribution.
// 
// THIS SOFTWARE IS PROVIDED BY Zeptonic Ek ``AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Zeptonic Ek OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// 
// The views and conclusions contained in the software and documentation are those of the 
// authors and should not be interpreted as representing official policies, 
// either expressed or implied, of Zeptonic Ek.


/**
 * Basklass för hantering av sidskrapning.
 * 
 * @category Golfscraper
 * @package  Golfscraper
 * @author   Anders Axelsson <anders@zeptonic.com>
 * @author   Mattias Tiger <mattias@zeptonic.com>
 * @link     http://www.zeptonic.com
 */
abstract class Scraper extends Page {
	/**
	 * @var Simple_html_dom html dom-objekt av sidskrapning.
	 */
	private $html;
	
	/**
	 * Startpunkt för hantering av sidskrapning.
	 * Anropar konstruktor hos förälder.
	 *
	 * @return Void
	 */
	public function __construct() {
		parent::__construct();
	}
	
	/**
	 * Hämtar html dom-objektet av sidskrapningen.
	 *
	 * @return Simple_html_dom
	 */
	protected function get_html() {
		return $this->html;
	}
	
	/**
	 * Sätter html dom-objektet av sidskrapningen.
	 *
	 * @param Simple_html_dom $html Html dom-objekt av sidskrapning
	 *
	 * @return Void
	 */
	protected function set_html($html) {
		$this->html = $html;
	}
	
	/**
	 * Hanterar inloggning via förfrågningsmetoden 'get'.
	 * Gör en inloggningsförfrågan till den skrapade-sidan med cookie 
	 * som skrivs till serverns hårddisk. Läs mer om cookiehantering 
	 * på @see{curl_get_request}.
	 *
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 */
	protected function handle_get_login($sid) {
		$url = REMOTE_BASE_URL2 . "/Webservices/LoginHandler.ashx?" . substr($_SERVER['QUERY_STRING'], strlen(MODE_KEY . "=" . MODE_VALUE_LOGIN));
		$request_info = "";
		$this->set_output($this->curl_get_request($url, $request_info, $sid));
	}
	
	/**
	 * Hanterar utloggning via förfrågningsmetoden 'get'.
	 * Tar bort cookie från användaren och cookie på servern samt gör en utloggningsförfrågan till
	 * huvudsidan. 
	 *
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 */
	protected function handle_get_logout($sid) {
		$this->delete_cookie($sid);
		$url = REMOTE_BASE_URL2 . "/Webservices/LoginHandler.ashx?" . substr($_SERVER['QUERY_STRING'], strlen(MODE_KEY . "=" . MODE_VALUE_LOGOUT));
		$this->set_output($this->curl_get_request($url));
	}
	
	/**
	 * Hanterar generella fall via förfrågningsmetoden 'get'.
	 * Om inget innehåll erhållits vid skrapningen beror detta med 
	 * stor sannolikhet på att inloggningssessionen har utgått, 
	 * cookien som använts tas då bort och ett nytt skrapförsök görs.
	 * Om innehåll erhållits skapas enn html dom-objekt som läggs i 
	 * instansvariabeln $html.
	 * Därefter anropas funktionen shape_scrap för att forma utseendet.
	 *
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 */
	protected function handle_get_generic($sid) {
		$parsing_url = $this->get_parsing_url();
		$base_url = Url_handler::base_url($parsing_url);
		$request_info = null;
		$content = $this->curl_get_request($parsing_url, $request_info, $sid);
		if(empty($content)) {
			// inloggningssession har antagligen utgått
			// cookien tas bort och ett nytt skrapförsök görs utan cookie
			// för att få upp inloggningssidan igen
			$this->delete_cookie($sid);
			$content = $this->curl_get_request($parsing_url, $request_info);
			if(empty($content)) {
				die("Error: could not get content from requested host.");
			}
		}
		$content = $this->correction_and_forming($content, $request_info, $sid);
		$this->set_output($content);
	}
	
	/**
	 * Hanterar generella fall via förfrågningsmetoden 'post' där formning av resultatet krävs.
	 *
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 */
	protected function handle_post_render($sid) {
		$parsing_url = $this->get_parsing_url();
		$base_url = Url_handler::base_url($parsing_url);
		$request_info = null;
		$content = $this->curl_post_request($parsing_url, $request_info, $sid);
		$render_cmd = $this->get_post_render_cmd();
		$content = $this->correction_and_forming($content, $request_info, $sid);
		$this->set_output($content);
	}
	
	/**
	 * Hanterar generella fall via förfrågningsmetoden 'post'.
	 *
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 */
	protected function handle_post_generic($sid) {
		$parsing_url = $this->get_parsing_url();
		$base_url = Url_handler::base_url($parsing_url);
		$request_info = null;
		$content = $this->curl_post_request($parsing_url, $request_info, $sid);
		$this->set_output($content);
	}
	
	/**
	 * Implementeras för att forma den skrapade sidans utseende.
	 *
	 * @param String $scrap_url Url som skrapats
	 * @param String $base_url Bas-url, tex http://domain.com
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 *
	 * @abstract
	 */
	protected abstract function shape_scrap($scrap_url, $base_url, $sid);
	
	/**
	 * Implementeras för att forma den skrapade sidans css-kod.
	 *
	 * @param String $content Innehåll i en css-fil 
	 *
	 * @return String
	 *
	 * @abstract
	 */
	protected abstract function shape_scrap_css($content);
	
	/**
	 * Implementeras för att forma den skrapade sidans javascript-kod.
	 *
	 * @param String $content Innehåll i en javascript-fil 
	 *
	 * @return String
	 *
	 * @abstract
	 */
	protected abstract function shape_scrap_javascript($content);
	
	/**
	 * Hämtar url som ska skrapas.
	 * Url:en hämtas från querystring, dess nyckel definieras genom 
	 * konstanten URL_KEY.
	 * Om ingen om ingen url efterfrågats så hämtas en förutbestämd url som
	 * definieras genom konstanten REMOTE_BASE_URL2.
	 * Querystring i url:en kan ha &-tecken mellan varje nyckel-värde-par
	 * vilket kan avkodas fel och blir &amp;. Dessa felkodade tecken ersätts med de rätta.
	 *
	 * @return String
	 */
	protected function get_parsing_url() {
		if(isset($_GET[URL_KEY])) {
			// gör inte en strtolower här ty målservern kan vara känslig för 
			// versaler och gemener i querystring
			$url = $_GET[URL_KEY];
			if(strtolower($url) == strtolower(REMOTE_BASE_URL1) || strtolower($url) == strtolower(REMOTE_BASE_URL2)) {
				return $url . "/site/booking/searchteetimes.aspx";
			} else {
				$scheme = parse_url($url, PHP_URL_SCHEME);
				$host = parse_url($url, PHP_URL_HOST);
				$path = parse_url($url, PHP_URL_PATH);
				$parsing_url = $scheme . "://" . $host . $path;
				
				$query = parse_url($url, PHP_URL_QUERY);
				if(!empty($query)) {
					$query = str_replace("&amp;", "&", $query);
					$parsing_url .= "?" . $query;
				}
				return $parsing_url;
			}
		} else {
			return REMOTE_BASE_URL2 . "/site/booking/searchteetimes.aspx";
		}
	}
	
	/**
	 * Skrapar och justerar css-filer. 
	 * De css-filer som finns i den skrapade html-innehållet laddas ner. 
	 * Relativa url:er till tex bilder justeras till absoluta url:er och
	 * sökvägen till sjävla css-filen ändras så sökvägen pekar till en 
	 * lokal kopia istället. 
	 * Dessutom anropas funktionen shape_scrap_css som formar css-kod.
	 *
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 */
	private function scrap_css($sid) {
		$html = $this->html;
		
		$dir = "css";
		// skapar mapp om den inte existerar
		if(!is_dir($dir)) {
			mkdir($dir);
		}
		
		$links = $html->find("link");
		foreach($links as $link) {
			if($link->type && stripos($link->type, "css") && $link->href) {
				$filepath = $dir . "/" . Url_handler::filename($link->href);
				if(!(is_file($filepath) && ENABLE_CACHING)) {
					// väljer rätt url till css-filen
					$base_url = "";
					switch(Url_handler::base_url(Url_handler::absolute_url($link->href, REMOTE_BASE_URL2))) {
						case REMOTE_BASE_URL1:
							$base_url = REMOTE_BASE_URL1;
							break;
						case REMOTE_BASE_URL2:
							$base_url = REMOTE_BASE_URL2;
							break;
					}
					
					// hitta full url till css-filen som ska laddas ner
					$content_url = $link->href;
					if(String::starts_with($content_url, "//")) {
						$content_url = "http:" . $content_url;
					} elseif(!String::starts_with($content_url, "http")) {
						$relative = "Styles/" . Url_handler::script_and_query($content_url);
						$content_url = Url_handler::absolute_url($relative, $base_url);
					}
					
					// laddar ner css-filen
					$content = $this->curl_get_request($content_url, $sid);
					
					// gör alla urler i css-filen till absoluta
					$content = Url_handler::backing_relative_to_abs_urls($content, $content_url, $base_url);
					$content = Url_handler::quoted_relative_to_abs_urls($content, $content_url, $base_url);
					
					// fixar "url(<relativ url>)" till absoluta urler
					$pattern = '/url\(([^"\'(http)].*?)\)/';
					$content = Url_handler::relative_to_abs_urls($pattern, $content, $content_url, $base_url);
					
					// formar css-kod
					$content = $this->shape_scrap_css($content);
					
					// sparar den modifierade css-filen
					$css_file = fopen($filepath, "w");
					fwrite($css_file, $content);
					fclose($css_file);
				
				}
				
				// sökväg pekas till lokal kopia
				$link->href = LOCAL_BASE_URL . "/" . $filepath;
			}
		}
	}
	
	/**
	 * Skrapar och justerar javascript-filer. 
	 * De javascript-filer som finns i den skrapade html-innehållet laddas ner. 
	 * Relativa url:er till tex bilder justeras till absoluta url:er och
	 * sökvägen till sjävla javascript-filen ändras så sökvägen pekar till en 
	 * lokal kopia istället. 
	 * Dessutom anropas funktionen shape_scrap_javascript som formar 
	 * javascript-kod.
	 *
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 */
	private function scrap_javascript($sid) {
		$html = $this->html;
		
		$dir = "js";
		// skapar mapp om den inte existerar
		if(!is_dir($dir)) {
			mkdir($dir);
		}
		
		$scripts = $html->find("script");
		
		foreach($scripts as $script) {
			$filepath = $dir . "/" . Url_handler::filename($script->src);
			$query = Url_handler::query($script->src);
			if($query)
				$query = "?" . $query;
			if(!empty($script->src)) {
				if(!(is_file($filepath) && ENABLE_CACHING)) {
					$base_url = REMOTE_BASE_URL2;
					$content_url = $script->src;
					if(String::starts_with($content_url, "//")) {
						$content_url = "http:" . $content_url;
					} elseif(!String::starts_with($content_url, "http")) {
						$content_url = $base_url . "/Scripts/" . Url_handler::filename($content_url);
					}
					
					// laddar ner javascript-filen
					$content = $this->curl_get_request($content_url, $sid);
					
					// gör alla urler i javascript-filen till absoluta
					$content = Url_handler::backing_relative_to_abs_urls($content, $content_url, $base_url);
					$content = Url_handler::quoted_relative_to_abs_urls($content, $content_url, $base_url);
					
					// formar javascript-kod
					$content = $this->shape_scrap_javascript($content);
					
					$js_file = fopen($filepath, "w");
					fwrite($js_file, $content);
					fclose($js_file);
				}
				
				// sökväg pekas till lokal kopia
				$script->src = LOCAL_BASE_URL . "/" . $filepath . $query;
			}
		}
	}
	
	/**
	 * Ändrar relativa url:er till absoluta url:er.
	 * Img-taggar och a-taggar.
	 *
	 * @param String $base_url Bas-url tex http://domain.com
	 *
	 * @return Void
	 */
	private function adjust_tag_paths($base_url) {
		$html = $this->html;
		$tags = $html->find("img");
		foreach($tags as $tag) {
			$tag->src = Url_handler::absolute_url($tag->src, $base_url);
		}
		$tags = $html->find("a");
		foreach($tags as $tag) {
			if(!String::starts_with($tag->href, "javascript")) {
				$tag->href = LOCAL_BASE_URL . "?" . URL_KEY . "=" . urlencode(Url_handler::absolute_url($tag->href, $base_url));
			}
		}
	}
	
	/**
	 * Tar bort cookie från klient och från server.
	 *
	 * @param String $sid Sessions-id
	 *
	 * @return Void
	 */
	private function delete_cookie($sid) {
		setcookie(COOKIE_NAME, "", time() - COOKIE_EXPIRE_TIME - 10);
		unlink(COOKIE_PATH . $sid . COOKIE_NAME);
		unset($sid);
	}
	
	/**
	 * Skrapar en url och returnerar resultatet samt fyller $request_info
	 * genom förfrågningsmetoden 'get'.
	 * Om $sid inte är tom då används cookie vid förfrågan. 
	 * Denna cookie sparas på serverns hårddisk.
	 *
	 * @param String $url Url som ska skrapas
	 * @param Array $request_info Förfrågningsinfo
	 * @param String $sid Sessions-id
	 *
	 * @return String
	 */
	private function curl_get_request($url, &$request_info = null, $sid = "") {
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		
		if(!empty($sid)) {
			curl_setopt($ch, CURLOPT_COOKIEJAR, COOKIE_PATH . $sid . COOKIE_NAME);
			curl_setopt($ch, CURLOPT_COOKIEFILE, COOKIE_PATH . $sid . COOKIE_NAME);
		}
		
		$output = curl_exec($ch);
		$request_info = curl_getinfo($ch);
		curl_close($ch);
		return $output;
	}
	
	/**
	 * Skrapar en url och returnerar resultatet samt fyller $request_info
	 * genom förfrågningsmetoden 'post'.
	 * Om $sid inte är tom då används cookie vid förfrågan. 
	 * Denna cookie sparas på serverns hårddisk.
	 *
	 * @param String $url Url som ska skrapas
	 * @param Array $request_info Förfrågningsinfo
	 * @param String $sid Sessions-id
	 *
	 * @return String
	 */
	private function curl_post_request($url, &$request_info = null, $sid = "") {
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
		curl_setopt($ch, CURLOPT_POST, true);
		curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($_POST));
		if(!empty($sid)) {
			curl_setopt($ch, CURLOPT_COOKIEJAR, COOKIE_PATH . $sid . COOKIE_NAME);
			curl_setopt($ch, CURLOPT_COOKIEFILE, COOKIE_PATH . $sid . COOKIE_NAME);
		}
		
		/*
    	$debug_file = fopen("curl_debug_post.txt","w");
	    curl_setopt($ch, CURLOPT_VERBOSE, true);
	    curl_setopt($ch, CURLOPT_STDERR, $debug_file);
        */
		
		$output = curl_exec($ch);
		$request_info = curl_getinfo($ch);
		curl_close($ch);
		return $output;
	}
	
	/**
	 * Korrigerar html från skrapningen och anropar funktioner för formning av det.
	 * Korrigerar url:er från relativa till absoluta pekandes till målservern
	 * i vissa fall till den lokala servern då caching sker eller för att förfrågningar 
	 * ska ske genom skrapning.
	 * Formulärfält och metadata läggs till för att fungera korrekt med målservern och 
	 * klientens webbläsare.
	 * 
	 * @param String $content Resultat från skrapning
	 * @param Array $request_info Förfrågningsinfo
	 * @param String $sid Sessions-id
	 * 
	 * @return String
	 */
	private function correction_and_forming($content, $request_info, $sid) {
		// den url som verkligen blev skrapad, redirects kan ha förekommit
		$scrap_url = $request_info["url"];
		$base_url = Url_handler::base_url($scrap_url);
		
		// bevara viewstate från all form av korrigering och formning
		$viewstate_placeholder = "{viewstate_placeholder}";
		$viewstate = $this->save_viewstate($content, $viewstate_placeholder);
		
		// == fixar sidans funktionalitet ==
		// gör alla urler i html-filen till absoluta
		$content = $this->add_metadata($content);
		$content = Url_handler::backing_relative_to_abs_urls($content, $scrap_url, $base_url);
		$content = Url_handler::quoted_relative_to_abs_urls($content, $scrap_url, $base_url);
		
		// skapar html dom-objekt av html-filen
		$this->html = str_get_html($content);
		
		$this->adjust_tag_paths($base_url);
		$this->correct_form_action($base_url);
		$this->add_eventtarget_eventargument();
		// == slut fixar sidans funktionalitet ==
		
		// == formning ==
		$this->scrap_css($sid);
		$this->scrap_javascript($sid);
		$this->shape_scrap($scrap_url, $base_url, $sid);
		// == slut formning ==
		
		// gör html dom-objektet till text och anger det som renderingsmaterial
		$html = $this->html;
		$content = $html->save();
		
		$this->restore_viewstate($content, $viewstate_placeholder, $viewstate);
		
		return $content;
	}
	
	/**
	 * Lägger till metadata i skrapninsresultatets header-tag.
	 * Lägger till metadata för att html-sidan ska fungera med 
	 * en webbläsare i en smartphone.
	 * 
	 * @param String $content Resultat från skrapning
	 * 
	 * @return String
	 */
	private function add_metadata($content) {
		return str_replace("<head>", '<head><meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">', $content);
	}
	
	/**
	 * Korrigerar url som formuläret ska göra förfrågan emot när det skickas.
	 * Förfrågningar ska göras emot den lokala servern.
	 * Förfrågningsläge (MODE_KEY) för och renderingskommando (POST_RENDER_CMD_KEY) till url:en.
	 * Sker genom html dom-objektet.
	 * 
	 * @param String $base_url Bas-url, tex http://domain.com
	 * 
	 * @return Void
	 */
	private function correct_form_action($base_url) {
		$html = $this->html;
		$e = $html->find('form', 0);
		if(isset($e)) {
			$e->action = LOCAL_BASE_URL . "?" . MODE_KEY . "=" . $this->get_mode() . "&" . POST_RENDER_CMD_KEY . "=" . $this->get_post_render_cmd() . "&" . URL_KEY . "=" . urlencode($base_url . "/Site/Booking/" . $e->action);
		}
	}
	
	/**
	 * Lägger till de gömda systemfält "__EVENTTARGET" och "__EVENTARGUMENT" för att
	 * den skrapade sidan ska fungera med målserverns asp.net-server om dessa inte finns 
	 * i skrapresultatet.
	 * Detta sker såvida ett formulär med namn "aspnetForm" existerar.
	 * Sker genom html dom-objektet.
	 * 
	 * @return Void
	 */
	private function add_eventtarget_eventargument() {
		$html = $this->html;
		$e = $html->find("#__EVENTTARGET", 0);
		$eventtarget_exist = isset($e);
		
		if(!$eventtarget_exist) {
			$content = $html->save();
			// infogar borttappade gömda fält som asp.net kräver, okänt varför dessa försvinner
			$pattern = '/<form name="aspnetForm"(.*?)>/ims';
			preg_match($pattern, $content, $matches);
			if(isset($matches[0])) {
				$form_opening_tag = $matches[0];
				$hidden_eventtarg_tag = '<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" />';
				$hidden_eventarg_tag = '<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" />';
				$form_and_hidden = $form_opening_tag . "\n" . $hidden_eventtarg_tag . "\n" . $hidden_eventarg_tag;
				$pos_form_start = strpos($content, $form_opening_tag);
				$pos_form_end = $pos_form_start + strlen($form_opening_tag);
				$content = substr($content, 0, $pos_form_start) . $form_and_hidden . substr($content, $pos_form_end, strlen($content));
				$this->html = str_get_html($content);
			}
		}
	}
	
	/**
	 * Klipper ut det gömda systemfältet "__VIEWSTATE" och returnerar detta samt lämnar
	 * en platshållare i dess ställe. Detta då värdet på fältet 
	 * riskerar att bli förstört vid url-korrigeringar sker.
	 * 
	 * @param String $content Resultat från skrapning
	 * @param String $placeholder En platshållare i fältets ställe, måste vara unikt för hela skrapningsresultatet
	 * 
	 * @return String
	 */
	private function save_viewstate(&$content, $placeholder) {
		// extrahera viewstate-taggen
		$pattern = '/(<input\s*type="hidden"\s*name="__VIEWSTATE"\s*id="__VIEWSTATE"\s*[^>]*\s*\/>)/ims';
		preg_match($pattern, $content, $matches);
		$viewstate = null;
		if(isset($matches[0])) {
			$viewstate = $matches[0];
		}
		
		if(isset($viewstate)) {
			// ersätt med en platshållare
			$replacement = $placeholder;
			$content = preg_replace($pattern, $replacement, $content);
		}
		
		return $viewstate;
	}
	
	/**
	 * Ersätter platshållaren för det gömda systemfältet "__VIEWSTATE".
	 * 
	 * @param String $content Resultat från skrapning
	 * @param String $placeholder En platshållare i fältets ställe
	 * @param String $viewstate Det gömda systemfältet som klippts ut och ska ersätta platshållaren
	 * 
	 * @return Void
	 */
	private function restore_viewstate(&$content, $placeholder, $viewstate) {
		$pattern = $placeholder;
		$replacement = $viewstate;
		$content = str_replace($pattern, $replacement, $content);
	}
}
?>